/*
Copyright 2010 Johan Maasing

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
 */
package nu.zoom.catonine.swing.tail;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import javax.swing.JSeparator;
import javax.swing.text.BadLocationException;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;

import nu.zoom.catonine.tail.TailerListener.LogEntry;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Document that remembers document positions in relation to file positions.
 * 
 * @author "Johan Maasing" &lt;johan@zoom.nu&gt;
 * 
 */
public class TailingDocument extends StyleRulesDocument implements
		TimestampDocument {
	private final static char[] EOL_ARRAY = "\n".toCharArray();

	private static final long serialVersionUID = 1L;
	private static final String STAMP_STYLE_NAME = "nu.zoom.catonine.swing.tail.STAMP_STYLE_NAME";
	private final Log log = LogFactory.getLog(getClass());
	// Content MUST always be linked with lowest file position first (head) and
	// ascending.
	protected LinkedList<FoldedDocumentLogEntry> content = new LinkedList<FoldedDocumentLogEntry>();

	public void lines(final List<LogEntry> lines) {
		log.debug("Adding " + lines.size() + " entries");
		final LinkedList<LogEntry> coalescedLines = coalesceLines(lines);
		log.debug("Coalesced lines to " + coalescedLines.size() + " entries");

		// Check if we need to update the last displayed entry with a new entry
		// from the tailer
		final LogEntry firstEntry = coalescedLines.peekFirst();
		if (firstEntry != null) {
			removeUpdatedEntries(firstEntry);
		}

		// We can now batch add all the coalesced entries
		final LinkedList<ElementSpec> batch = new LinkedList<ElementSpec>();
		int offsetCounter = getLength();
		for (final LogEntry logEntry : coalescedLines) {
			offsetCounter += batchEntry(offsetCounter, batch, logEntry);
		}
		ElementSpec[] batchArray = batch.toArray(new ElementSpec[batch.size()]);
		try {
			log.debug("Inserting batch of " + batchArray.length
					+ " element specifications");
			insert(getLength(), batchArray);
		} catch (BadLocationException e) {
			log.error("Unable to insert batch", e);
		}
	}

	/**
	 * @param logEntry
	 */
	private int batchEntry(final int offsetCounter,
			final LinkedList<ElementSpec> batch, final LogEntry logEntry) {

		final String entryContent = logEntry.content;
		final StyleSpecification styleSpecification = getStyleSpecification(entryContent);
		int offsetChange = 0;

		// Style name == null means supress this entry
		if (styleSpecification != null) {
			if (!styleSpecification.supress) {
				final Style style = getStyle(styleSpecification.styleName);
				final boolean endsWithNewLine = entryContent.endsWith("\n");
				if (endsWithNewLine && entryContent.length() == 1) {
					log.debug("One newline entry");
					return 0;
				}
				String[] split = entryContent.split("\\n");
				if (split != null) {
					final FoldedDocumentLogEntry documentLogEntry = new FoldedDocumentLogEntry();
					int entryFileLength = 0;
					int entryDocumentLength = 0;
					documentLogEntry.unfoldable = split.length < 2;
					for (int i = 0; i < split.length; i++) {
						// TODO: Replace splitting with regexp find for \n and
						// then construct element specs with offsets and length
						// into the chararray.
						// Splitting removes newlines, the last (or only) part
						// might or might not have ended in a newline. This also
						// removes consequtive newlines inside the logentry
						char[] charArray = ((endsWithNewLine || (i < split.length - 1)) ? (split[i] + "\n")
								: split[i]).toCharArray();
						if (i == 0) {
							addParagraphBreak(batch);
						} else {
							addLineBreak(batch);
						}
						if ((i == 0) || (!styleSpecification.fold)) {
							batch.add(new ElementSpec(style,
									ElementSpec.ContentType, charArray, 0,
									charArray.length));
							entryDocumentLength += charArray.length;
							documentLogEntry.folded = false;
						} else {
							documentLogEntry.folded = true;
							documentLogEntry.foldedContent.add(charArray);
						}
						entryFileLength += charArray.length;
					}
					documentLogEntry.fileOffset = logEntry.position;
					documentLogEntry.fileLength = entryFileLength;
					documentLogEntry.styleGUID = styleSpecification.styleName;
					documentLogEntry.documentOffset = offsetCounter;
					documentLogEntry.documentLength = entryDocumentLength;
					offsetChange = documentLogEntry.documentLength;
					this.content.add(documentLogEntry);
				}
			}
		}
		return offsetChange;
	}

	private LinkedList<LogEntry> coalesceLines(List<LogEntry> list) {
		final LinkedList<LogEntry> result = new LinkedList<LogEntry>();
		for (final LogEntry newEntry : list) {
			final LogEntry existingEntry = result.peekLast();
			if (existingEntry != null) {
				final long existingPosition = existingEntry.position;
				final long newPosition = newEntry.position;
				if (existingPosition == newPosition) {
					result.removeLast();
				} else if (existingPosition > newPosition) {
					log.warn("Coalesce lines detected out of order entries");
				}
			}
			result.add(newEntry);
		}
		return result;
	}

	/**
	 * Checks if the log entry indicates a file position we already have
	 * displayed in the document.
	 */
	private synchronized void removeUpdatedEntries(final LogEntry logEntry) {
		DocumentLogEntry lastEntry = this.content.peekLast();
		boolean keepGoing = (lastEntry != null);
		while (keepGoing) {
			if (lastEntry.fileOffset >= logEntry.position) {
				DocumentLogEntry removedEntry = this.content.removeLast();
				try {
					remove(removedEntry.documentOffset,
							removedEntry.documentLength);
				} catch (BadLocationException e) {
					log.error(e);
				}
				lastEntry = this.content.peekLast();
				keepGoing = (lastEntry != null);
			} else {
				keepGoing = false;
			}
		}
	}

	/**
	 * Indirect callback from the tailer.
	 */
	public void tailerHasBeenReset() {
		// Clear the document
		content.clear();
		int start = 0;
		int length = getLength();
		try {
			remove(start, length);
		} catch (BadLocationException e) {
			// if this happens there is something seriously wrong
			// with the lock on the document
			throw new IllegalStateException(e);
		}
	}

	@Override
	public void addStampAfterLast() {
		FoldedDocumentLogEntry documentLogEntry = new FoldedDocumentLogEntry();
		documentLogEntry.synthetic = true;
		documentLogEntry.folded = false;
		documentLogEntry.styleGUID = STAMP_STYLE_NAME;
		documentLogEntry.documentOffset = getLength();
		documentLogEntry.documentLength = EOL_ARRAY.length;
		SimpleAttributeSet stampStyle = new SimpleAttributeSet(
				getStyle(DEFAULT_CHARACTER_STYLE_NAME));
		StyleConstants.setComponent(stampStyle, new JSeparator(
				JSeparator.HORIZONTAL));
		final ElementSpec contentElementSpec = new ElementSpec(stampStyle,
				ElementSpec.ContentType, EOL_ARRAY, 0, EOL_ARRAY.length);

		LinkedList<ElementSpec> batch = new LinkedList<ElementSpec>();
		addLineBreak(batch);
		batch.add(contentElementSpec);
		try {
			insert(documentLogEntry.documentOffset,
					batch.toArray(new ElementSpec[batch.size()]));
		} catch (BadLocationException e) {
			log.error("Unable to append entry - concurrency error?", e);
		}
		this.content.addLast(documentLogEntry);
	}

	public synchronized void toggleFold(int documentOffset)
			throws BadLocationException {
		int offsetChange = 0;
		for (FoldedDocumentLogEntry docEntry : this.content) {
			if (offsetChange == 0) {
				// not found the entry yet, keep searching
				final int docEntryEndposition = docEntry.documentOffset
						+ docEntry.documentLength;
				if ((documentOffset >= docEntry.documentOffset)
						&& (documentOffset < docEntryEndposition)) {
					if (docEntry.folded) {
						offsetChange = unfoldEntry(docEntry);
					} else {
						offsetChange = foldEntry(docEntry);
					}
				}
			} else {
				// Adjusting
				docEntry.fileOffset += offsetChange;
			}
		}
	}

	private int foldEntry(FoldedDocumentLogEntry docEntry)
			throws BadLocationException {
		final String content = getText(docEntry.documentOffset,
				docEntry.documentLength);
		final String[] split = content.split("\\n");
		if (split.length > 1) {
			ArrayList<char[]> foldedContent = new ArrayList<char[]>();
			int offsetChange = 0;
			for (int i = 1; i < split.length; i++) {
				offsetChange += split[i].length();
				foldedContent.add(split[i].toCharArray());
			}
			// Add one to move past the newline (really must fix this split
			// stuff to handle newlines
			replace(docEntry.documentOffset + split[0].length() + 1,
					offsetChange, "", null);
			docEntry.documentLength = split[0].length();
			docEntry.folded = true;
			return -offsetChange;
		} else {
			// Can't fold a single line
			return 0;
		}
	}

	private int unfoldEntry(FoldedDocumentLogEntry docEntry) {
//		ArrayList<char[]> foldedContent = docEntry.foldedContent;
//
		return 0;
	}
}
